"""
Describes the most abstract notions of canvas elements, simplifying positioning and rendering.
"""
import collections

import pygame

from events import EventProcessor

#Useful geometric objects
Area = collections.namedtuple('Rectangle', ['left', 'top', 'right', 'bottom'])
Rectangle = collections.namedtuple('Area', ['left', 'top', 'width', 'height'])
Area.to_rectangle = lambda self : Rectangle(self.left, self.top, self.right - self.left, self.top - self.bottom)
Area.to_area = lambda self : self
Rectangle.to_area = lambda self : Area(self.left, self.top, self.left + self.width, self.top + self.height)
Rectangle.to_rectangle = lambda self : self

def _compute_bounding_rect(old_rect, new_rect, old_rect_offset=(0, 0), new_rect_offset=(0, 0)):
    (o_l, o_t, o_w, o_h) = old_rect
    (oo_l, oo_t) = old_rect_offset
    o_l = o_l + oo_l
    o_t = o_t + oo_t
    
    (n_l, n_t, n_w, n_h) = new_rect
    (on_l, on_t) = new_rect_offset
    n_l = n_l + on_l
    n_t = n_t + on_t
    
    l = min(o_l, n_l) #left
    t = min(o_t, n_t) #top
    return Rectangle(
     l, t,
     max(o_w + o_l, n_w + n_l) - l, #width
     max(o_h + o_t, n_h + n_t) - l #height
    )
    
class ElementManager(object):
    """
    Manages every defined element, to facilitate order-resolution and disposal.
    """
    _elements = None #A set of elements that are currently present
    
    def __init__(self):
        self._elements = set()
        
    def render(self):
        """
        This should, before actually rendering surfaces, compile a list of dirty areas (ask the
        element if it has changed, and, if so, add its area(s, in the case of a moving object) to a
        map of the screen, which then uses the location and dimensions of each element to render
        only within the boundaries that have changed.
        
        When computing the boundaries, use min left/top and max right/bottom to build a single
        update rectangle, for simplicity. In most cases, what will change is just the text area or
        a bouncing cursor, which will yield small areas. The worst case is that there's an animated
        dial in the upper left and a cursor on the lower right, in which case the entire frame
        (almost) is being redrawn, but the computation time for dirty rects should be pretty minor,
        making this a slight penalty over a naive rendering strategy.
        """
        dirty = None
        for element in self._elements:
            #if element.visible: #Flipping visibility toggles dirty
            element_dirty = element.dirty
            if element_dirty:
                if dirty:
                    dirty = _compute_bounding_rect(dirty, element_dirty)
                else:
                    dirty = element_dirty
                    
        if dirty: #Avoid redrawing if nothing changed
            for element in sorted(self._elements):
                element.render(dirty)
                
    def add_element(self, element):
        """
        """
        self.shift_elements(element)
        self._elements.add(element)
        
    def remove_element(self, element):
        """
        """
        self._elements.discard(element)
        
    def shift_elements(self, element):
        """
        If `element`'s z-order conflicts with another element, every element below it gets reduced
        by 1 until a gap is reached.
        """
        
class Surface(pygame.Surface):
    _dirty_rect = None #The rect that was dirtied or None if nothing changed
    _subsurfaces = None #A collection of all known subsurfaces, which should be culled as they're garbage-collected
    
    def __init__(self, *args, **kwargs):
        pygame.Surface.__init__(self, *args, **kwargs)
        self._subsurfaces = set()
        
    def _get_dirty(self):
        dirty_rect = self._dirty_rect
        for subsurface in self._subsurfaces: #Subsurfaces are always considered dirty
            (l, t) = subsurface.get_offset()
            (w, h) = subsurface.get_size()
            dirty_rect = _compute_bounding_rect(dirty_rect, (l, t, w, h))
        return dirty_rect
    def _set_dirty(self, dirty_rect):
        if dirty_rect is True:
            self._dirty_rect = self.get_rect()
        elif dirty_rect is False:
            self._dirty_rect = None
        else:
            if not self._dirty_rect:
                self._dirty_rect = dirty_rect
            else:
                self._dirty_rect = _compute_bounding_rect(self._dirty_rect, dirty_rect)
    dirty = property(_get_dirty, _set_dirty)
    
    def subsurface(self, *args, **kwargs):
        subsurface = pygame.subsurface(self, *args, **kwargs)
        
        ss_abandon = self._subsurface_abandon
        def s_del(nil):
            _del = hasattr(pygame.Surface, '__del__') and getattr(pygame.Surface, '__del__')
            if _del:
                _del(subsurface)
            ss_abandon(subsurface)
            
        subsurface.__del__ = s_del
        
    def _subsurface_abandon(self, subsurface):
        print("Beginning subsurface-abandon")
        self._subsurfaces.discard(subsurface)
        print("Ending subsurface abandon")
        
    def blit(self, source, dest, area=None, special_flags=0):
        if len(dest) == 2:
            (top, left) = dest
        else:
            (top, left, _, _) = dest
            
        if area:
            (_, _, width, height) = area
        else:
            (width, height) = source.get_size()
            
        self.dirty = (top, left, width, height)
        return pygame.Surface.blit(self, source, dest, area, special_flags)
        
    def fill(self, color, rect=None, special_flags=0):
        if rect:
            self.dirty = rect
        else:
            self.dirty = True
        return pygame.Surface.fill(self, color, rect, special_flags)
        
    def scroll(self, *args, **kwargs):
        self.dirty = True
        return pygame.Surface.scroll(self, *args, **kwargs)
        
    def set_alpha(self, *args, **kwargs):
        self.dirty = True
        return pygame.Surface.set_alpha(self, *args, **kwargs)
        
    def set_at(self, pos, color):
        (x, y) = pos
        self.dirty = (x, y, 1, 1)
        return pygame.Surface.set_at(self, pos, color)
        
    def set_colorkey(self, *args, **kwargs):
        self.dirty = True
        return pygame.Surface.set_colorkey(self, *args, **kwargs)
        
    def set_palette(self, *args, **kwargs):
        self.dirty = True
        return pygame.Surface.set_palette(self, *args, **kwargs)
        
    def set_palette_at(self, *args, **kwargs):
        self.dirty = True
        return pygame.Surface.set_palette_at(self, *args, **kwargs)
        
        
class Element(EventProcessor):
    _render_surface = None
    
    _translucent = False
    _managed_surface = None #Subclasses need to assign this

    _x = 0
    _y = 0
    
    _zorder = -1 #The order in which elements are drawn
    _visible = True

    def __init__(self, render_surface, zorder=-1):
        EventProcessor.__init__(self)
        self._render_surface = render_surface
        self._zorder = zorder

    def __cmp__(self, o):
        return cmp(self.zorder, o.zorder)
        
    def _get_surface(self):
        return self._managed_surface
    def _set_surface(self, surface):
        if not isinstance(surface, Surface):
            raise ValueError("Unable to use non-Surface element in canvas rendering")
        #Needs to merge dirty with the previous surface's location, if any; needs to be aware of multiple calls within a single frame.
        self._managed_surface = surface
    _surface = property(_get_surface, _set_surface)
    surface = _surface
    
    def destroy(self):
        """
        Unparents from the manager and releases resources.
        """

    def get_zorder(self):
        return self._zorder
        
    def set_zorder(self, zorder):
        """
        Call manager.shift_elements(self) after the zorder has been changed.
        """
        self._zorder = zorder
        
    def set_above(self, objects):
        """
        Sets the z-order of this element to be greater than all of the other objects, using in-place
        shuffling, like a bubble-sort.
        """
        
    def set_below(self, objects):
        """
        Sets the z-order of this element to be lower than all of the other objects, using in-place
        shuffling, like a bubble-sort.
        """
        
    def set_location(self, offset_x=0, offset_y=0, invert_x=False, invert_y=False, centre_x=False, centre_y=False):
        """
        Sets the location of this element

        If `x` or `y` are given, they shift relative to top-left; if `invert_x` or `invert_y` are
        set, the opposite edge of the canvas is used instead. If `centre_x` or `centre_y` are set,
        all calculations are done relative to the the element centred as indicated, with its current
        surface dimensions.
        """
        (width, height) = self._screen.get_size()
        (s_width, s_height) = self._managed_surface.get_size()
        
        if centre_x:
            x = int((width - s_width) / 2)
        else:
            if invert_x:
                x = width - s_width
            else:
                x = 0
        
        if centre_y:
            y = int((height - s_height) / 2)
        else:
            if invert_y:
                y = height - s_height
            else:
                y = 0
                
        self._x = x + offset_x
        self._y = y + offset_y
        
    def _get_visible(self):
        return self._visible
    def _set_visible(self, visible):
        self._visible = visible
        self._render_surface.dirty = True
    visible = property(_get_visible, _set_visible)
    
    @property
    def dirty(self):
        surface_dirty = self._surface.dirty
        if surface_dirty:
            (left, top, width, height) = surface_dirty
            surface_dirty = Rectangle(left + self._x, top + self._y, width, height)
        return surface_dirty
        
    def render(self, region):
        """
        If this element is visible, compute the subset of this element that falls within the
        ``Rectangle`` `region` and render that to the underlying surface.
        
        After being rendered, the surface is no longer considered "dirty".
        """
        if self._visible:
            left = self._x
            top = self._y
            (width, height) = self._managed_surface.get_size()
            right = width + left
            bottom = height + top
            
            region_area = region.to_area()
            if (
             left > region_area.right or
             top > region_area.bottom or
             right < region_area.left or
             bottom < region_area.top
            ): #No chance of overlap
                return
                
            draw_left = max(left, region_area.left)
            draw_top = max(top, region_area.top)
            draw_right = min(right, region_area.right)
            draw_bottom = min(bottom, region_area.bottom)
            print (draw_left, draw_top), (
             draw_left - left,
             draw_top - top,
             draw_right - draw_left,
             draw_bottom - draw_top,
            )
            self._render_surface.blit(self._managed_surface, (draw_left, draw_top), (
             draw_left - left,
             draw_top - top,
             draw_right - draw_left,
             draw_bottom - draw_top,
            ))
        self._managed_surface.dirty = False
        
class MovingElement(Element):
    """
    When calculating dirty status, the old location and new location need to be
    bounded together.
    
    Should be merged with Element, with _set_surface() implicitly tracking difference.
    """
    
class ResizingElement(MovingElement):
    """
    Extends MovingElement's dirty logic (adding dimensions to locations).
    
    Adds methods to change the underlying surface.
    
    Should be merged with Element; see MovingElement.
    """
    
